/*    
    LogParser - Log File Parser
    Copyright (C) 2007  Alois Kraus
  
    This source code is published under the Microsoft Permissive License (Ms-PL)
    http://www.microsoft.com/resources/sharedsource/licensingbasics/permissivelicense.mspx
*/


using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Xml.XPath;
using Microsoft.Practices.EnterpriseLibrary.Logging.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using EntLibContrib.Logging.Configuration;

namespace EntLibContrib.Logging.Parser
{
    /// <summary>
    /// Read the logging configuration and build from each TextFormatter message template a corresponding regular expression which
    /// is the tokenizer for the log file or the windows event log.
    /// </summary>
    public class RegularExpressionBuilder
    {
        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
        /// </summary>
        public const string TimeStampToken     = "timestamp";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string MessageToken       = "message";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
        /// </summary>
        public const string CategoryToken      = "category";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
        /// </summary>
        public const string PriorityToken      = "priority";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
        /// </summary>
        public const string EventIdToken       = "eventid";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
        /// </summary>
        public const string SeverityToken      = "severity";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string TitleToken         = "title";
        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string ErrorMessagesToken = "errorMessages";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string MachineToken       = "machine";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string AppDomainNameToken = "appDomain";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string ProcessIdToken     = "processId";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string ProcessNameToken   = "processName";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string ThreadNameToken    = "threadName";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string Win32ThreadIdToken = "win32ThreadId";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string ActivityidToken    = "activity";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string NewLineToken       = "{newline}";

        /// <summary>
        /// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
        /// </summary>
        public const string TabToken           = "{tab}";

        static readonly string[] StringTokens = new string[] {  TimeStampToken,
		                                        MessageToken,
		                                        CategoryToken,
		                                        TitleToken,
		                                        ErrorMessagesToken,
		                                        MachineToken,
		                                        AppDomainNameToken,
		                                        ProcessNameToken,
		                                        ThreadNameToken,
    		                                    ActivityidToken 
                                             };

        // Allowed values of TraceEventType
        const string SeverityTokenValues = "Critical|Error|Warning|Information|Verbose|Start|Stop|Suspend|Resume|Transfer";

        static readonly string[] NumericalTokens = new string[] {
		                                        PriorityToken,
		                                        EventIdToken,
		                                        ProcessIdToken,
		                                        Win32ThreadIdToken,
                                                };


        /// <summary>
        /// 
        /// </summary>
        public const string DictionaryParamToken = "dictionary";

        const string TimeStampParamToken = "{timestamp(";
        private const string LocalStartDelimiter = "local";
        private const string LocalStartDelimiterWithFormat = "local:";

        const string ReflectedParamPropertyToken = "{property(";


        const string LoggingSectionName = "loggingConfiguration";

        LoggingSettings myLogSettings;  // Deserialized logging settings from config file

        private Dictionary<string, List<Regex>> myFileRegularExpressions;

        /// <summary>
        /// This is the result of our hard regular expression building after we did
        /// parse the logging configuration and created from the TextFormatter templates the corresponding regular
        /// expression lists.
        /// Key is the event log name, or the log file name (without path). The values are a list regular expressions which  
        /// represent the tokenizer for this file.
        /// </summary>
        public Dictionary<string, List<Regex>> FileRegularExpressions
        {
            get { return myFileRegularExpressions; }
        }

        /// <summary>
        /// Map of template instance names to template string
        /// </summary>
        Dictionary<string, string> myTemplateNameToTemplate;

        /// <summary>
        /// Initializes a new instance of the <see cref="RegularExpressionBuilder"/> class which retrieves the logging
        /// configuration from the current applications App.Config file. Not really a real world scenario for
        /// an application which logs and the parses its own previously written log file but it makes testing a
        /// bit easier.
        /// </summary>
        public RegularExpressionBuilder():this(new SystemConfigurationSource())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RegularExpressionBuilder"/> class.
        /// </summary>
        /// <param name="source">The Enterprise Library configuration source which contains the logging settings.</param>
        public RegularExpressionBuilder(IConfigurationSource source)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            Init(source);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RegularExpressionBuilder"/> class.
        /// </summary>
        /// <param name="cfgFile">The config file containing the logging settings.</param>
        public RegularExpressionBuilder(string cfgFile)
        {
            if (String.IsNullOrEmpty(cfgFile))
                throw new ArgumentException("cfgFile parameter was null or empty!");

            FileConfigurationSource source = new FileConfigurationSource(cfgFile);
            Init(source);
        }

        void Init(IConfigurationSource source)
        {
            myFileRegularExpressions = new Dictionary<string, List<Regex>>();
            myTemplateNameToTemplate = new Dictionary<string, string>();

            myLogSettings = source.GetSection(LoggingSectionName) as LoggingSettings;
            if (myLogSettings == null)
                throw new InvalidOperationException("Could not read logging settings from configuration!");

            BuildTemplateInstanceList();
            CreateRegexFromListenerConfig();
        }

        /// <summary>
        /// Builds the template instance list. It does iterate over all text formatters which have been read
        /// from the logging configuration and creates a regular expression from every message template. Each
        /// process template is stored in a dictionar with its name.
        /// </summary>
        protected virtual void BuildTemplateInstanceList()
        {
            foreach (FormatterData o in myLogSettings.Formatters)
            {
                ITemplateTextFormatterData textFormatter = CategoryLogFileReader.GetTextFormatterData(o);
                if (textFormatter != null)
                {
                    myTemplateNameToTemplate.Add(textFormatter.Name, textFormatter.Template);
                    //Console.WriteLine("Template: " + textFormatter.Template);
                }
            }
        }

        /// <summary>
        /// Create the final message template strings. At first we get from the FileListeners the header and footer strings which are added to the
        /// complete template. Then we create for each configured log file from the full template string the regular expression which can parse
        /// every message written into the log file or event logs.
        /// </summary>
        protected virtual void CreateRegexFromListenerConfig()
        {
            foreach (TraceListenerData tracedata in myLogSettings.TraceListeners)
            {
                FlatFileTraceListenerData filedata = tracedata as FlatFileTraceListenerData;
                RollingFlatFileTraceListenerData rollingfiledata = tracedata as RollingFlatFileTraceListenerData;
                FormattedEventLogTraceListenerData eventlogData = tracedata as FormattedEventLogTraceListenerData;

                if (filedata != null)
                {
                    string completeTemplate = ConcatCompleteTemplate(
                        filedata.Header,
                        myTemplateNameToTemplate[filedata.Formatter],
                        filedata.Footer);
                    AddFileNameAndTemplate(Path.GetFileName(filedata.FileName), completeTemplate);
                }
                else if (rollingfiledata != null)
                {
                    string completeTemplate = ConcatCompleteTemplate(
                        rollingfiledata.Header,
                        myTemplateNameToTemplate[rollingfiledata.Formatter],
                        rollingfiledata.Footer);
                    AddFileNameAndTemplate(Path.GetFileName(rollingfiledata.FileName), completeTemplate);
                }
                else if (eventlogData != null)
                {
                    AddFileNameAndTemplate(eventlogData.Log, myTemplateNameToTemplate[eventlogData.Formatter]);
                }
                else
                {
                    Debug.Print("Ignored trace listener {0}, type {1} because it was no file or event log listener with a formatter", tracedata.Name, tracedata.Type);
                }
            }
        }

        /// <summary>
        /// Concatenate the complete template text with header and footer.
        /// </summary>
        /// <param name="listenerHeader">Header from the trace listener</param>
        /// <param name="formatterTemplate">Template from the formatter</param>
        /// <param name="listenerFooter">Footer from the listener</param>
        /// <returns>the complete template text with header and footer</returns>
        public static string ConcatCompleteTemplate(string listenerHeader, string formatterTemplate, string listenerFooter)
        {
            string completeTemplate = "";
            if (!string.IsNullOrEmpty(listenerHeader))
                completeTemplate += listenerHeader + "\r\n";
            completeTemplate += formatterTemplate + "\r\n";
            if (!string.IsNullOrEmpty(listenerFooter))
                completeTemplate += listenerFooter;
            return completeTemplate;
        }

        /// <summary>
        /// Add for this log file a template to the internal list. This function does create from the given message
        /// template the regular expression which can be used later.
        /// </summary>
        /// <param name="fileName">Name of the log file the template was used for.</param>
        /// <param name="template">The template string.</param>
        public virtual void AddFileNameAndTemplate(string fileName, string template)
        {
            if (!myFileRegularExpressions.ContainsKey(fileName))
            {
                List<Regex> list = new List<Regex>();
                list.Add(BuildRegularExpressionFromTemplate(template));
                myFileRegularExpressions.Add(fileName, list);
            }
            else
            {
                List<Regex> list = myFileRegularExpressions[fileName];
                list.Add(BuildRegularExpressionFromTemplate(template));
            }
        }

        /// <summary>
        /// Escapes the token name with its enclosing tags plus the regular expression escape char for the { char
        /// </summary>
        /// <param name="str">The token name.</param>
        /// <returns>Escaped token</returns>
        protected static string EscapeToken(string str)
        {
            if (String.IsNullOrEmpty(str))
                Debug.Assert(false, "string str is null or empty!");

            return "\\{" + str + "}";
        }


        /// <summary>
        /// Tret the special timestamp( and property( tokens as blobs to create a correct regex from it.
        /// </summary>
        /// <param name="regexString">The current regular expresson message template string.</param>
        /// <param name="token">The token name.</param>
        /// <param name="varName">Name of the capturing group which will match the formatted token string.</param>
        /// <returns>Changed regular expression message template</returns>
        protected static string TreatToken(string regexString, string token, string varName)
        {
            int start = regexString.IndexOf(token);
            if( start != -1 )
            {
                int stop = regexString.IndexOf('}', start);
                Debug.Assert( stop == -1, String.Format("Could not find stop token in {0}", token) );
                if( stop != -1 )
                {
                    return regexString.Remove(start, stop-start).Insert(start,"(?<" + varName + ">.*?)");
                }
            }

            return regexString;
        }

        /// <summary>
        /// Treat the dictionary token as blob data which will be stored as one string in the dictionary
        /// </summary>
        /// <param name="regexString">Current Regular expression string</param>
        /// <returns>Replaced dictionary token if present.</returns>
        protected static string TreatDictionary(string regexString)
        {
            // Check if escaped Dictionary token is present "\{dictionary\("
            int idx = regexString.IndexOf("\\{" + DictionaryParamToken + "\\(");
            if (idx != -1)
            {
                int openCount = 0;
                // search for the end of the dictionary
                int i;
                bool wasGreaterZero = false;
                for (i = idx; i < regexString.Length; i++)
                {
                    if (regexString[i] == '{')
                    {
                        openCount++;
                        wasGreaterZero = true;
                    }
                    else if (regexString[i] == '}')
                        openCount--;

                    if (openCount == 0 && wasGreaterZero)
                    {
                       break;
                    }
                }

                // when opencount is not zero the template was garbage.
                if (openCount == 0)
                {
                    // remove the dictionary formatting and treat all formatted key/value pairs as one blob
                    return regexString.Remove(idx, i - idx+1).Insert(idx, "(?<"+DictionaryParamToken+">.*?)");
                }
                else
                {
                    Debug.Assert(false, "Dictionary start was found but no end could be determined");
                }
            }

            return regexString;
        }

        /// <summary>
        /// Builds the regular expression from a message template.
        /// </summary>
        /// <param name="template">The template.</param>
        /// <returns>A regular expression which can match every message which was written with this template</returns>
        virtual protected Regex BuildRegularExpressionFromTemplate(string template)
        {
            return DoBuildRegularExpressionFromTemplate(template);
        }
        /// <summary>
        /// Builds the regular expression from a message template.
        /// </summary>
        /// <param name="template">The template.</param>
        /// <returns>A regular expression which can match every message which was written with this template</returns>
        public static Regex DoBuildRegularExpressionFromTemplate(string template)
        {
            StringBuilder regexString = new StringBuilder(template);
            regexString.Replace(NewLineToken, Environment.NewLine);
            regexString.Replace(TabToken, "\t");

            // escape the template so not other tokens are recognized as regex sequence later
            regexString = new StringBuilder(Regex.Escape(regexString.ToString()));

            // numerical tokens are treated different to allow faster and better performing matching
            foreach (string numToken in NumericalTokens)
            {
                regexString.Replace(EscapeToken(numToken),String.Format("(?<{0}>[+-]?\\d+)",numToken));
            }

            // Each string token is as greedy it should be
            foreach(string stringToken in StringTokens)
            {
                regexString.Replace(EscapeToken(stringToken), String.Format("(?<{0}>.*?)", stringToken));
            }

            // Severity is special because its value is only a fixed set of values.
            regexString.Replace(EscapeToken(SeverityToken), String.Format("(?<{0}>{1})", SeverityToken, SeverityTokenValues));

            // The dictionary(....) token is treated as blob
            string curRegex = TreatDictionary(regexString.ToString());


            curRegex = TreatToken(curRegex, TimeStampParamToken, "timestamptoken");
            curRegex = TreatToken(curRegex, ReflectedParamPropertyToken, "propertytoken");
            
            // Finally we can create the regular expression which will match every single message from a log file or the windows
            // event log
            Regex rex = new Regex(curRegex, RegexOptions.Singleline);

            return rex;
        }
    }
}
